package XNAT::DICOM::Series;
@ISA = ('XNAT::DICOM::Entity');

use strict;
use Carp qw(croak carp cluck confess);
use Cwd qw( abs_path getcwd );

require XNAT::DICOM::File;
require XNAT::DICOM::Session;

# Used to keep track of objects.
my %loaded_by_uid;
my %loaded_by_label;

# Which DICOM Headers have to be the same across the Series?
my $default_immutable_headers =
	{
	 AccessionNumber => 1,
	 BitsAllocated => 1,
	 BitsStored => 1,
	 Columns => 1,
	 EchoTime => 1,
	 EchoTrainLength => 1,
	 FlipAngle => 1,
	 HighBit => 1,
	 ImageType => 1,
	 ImagingFrequency => 1,
	 InversionTime => 1,
	 MIMEType => 1,
	 MRAcquisitionType => 1,
	 MagneticFieldStrength => 1,
	 Manufacturer => 1,
	 ManufacturersModelName => 1,
	 NumberOfAverages => 1,
	 NumberOfPhaseEncodingSteps => 1,
	 NumberOfSeriesRelatedInstances => 1,
	 PatientPosition => 1,
	 PatientsName => 1,
	 PercentPhaseFieldOfView => 1,
	 PercentSampling => 1,
	 PhotometricInterpretation => 1,
	 PixelBandwidth => 1,
	 PixelSpacing => 1,
	 ProtocolName => 1,
	 ReceiveCoilType => 1,
	 RepetitionTime => 1,
	 Rows => 1,
	 SamplesPerPixel => 1,
	 ScanOptions => 1,
	 ScanningSequence => 1,
	 SequenceName => 1,
	 SequenceVariant => 1,
	 SeriesDate => 1,
	 SeriesDescription => 1,
	 SeriesInstanceUID => 1,
	 SeriesNumber => 1,
	 SeriesTime => 1,
	 SliceThickness => 1,
	 SpacingBetweenSlices => 1,
	 SpecificCharacterSet => 1,
	 StationName => 1,
	 VariableFlipAngleFlag => 1,
#	 XNATSeriesType => 0,
	 XResolution => 1,
	 YResolution => 1,
	};

my %immutable_headers;
my %_seriesDescription_2_XNAT_Type;
my @_seriesDescription_2_XNAT_Type;

sub _map_series_desc {
	my ($re,$type,$immutables,@to_delete) = @_;
	$_seriesDescription_2_XNAT_Type{$re} = $type;
	push @_seriesDescription_2_XNAT_Type, $re;
	$immutable_headers{$re} = {%$immutables};
	foreach(@to_delete) {
		delete $immutable_headers{$re}->{$_};
	}
}

_map_series_desc(qr/^ge_func_3mmS_test$|^ge_func_3mmS_124tp/i,'BOLD-TEST',$default_immutable_headers);
_map_series_desc(qr/^[Ll]ocal.*/i,'LOCALIZER',$default_immutable_headers);
_map_series_desc(qr/^MPRAGE.*$|^T1_MPRAGE.*$|^t1_mpr.*$/i,'MPRAGE',$default_immutable_headers);
_map_series_desc(qr/^t2_spc.*$|T2_SPACE/i,'T2 SPACE',$default_immutable_headers);
_map_series_desc(qr/^ep2d.*/i,'T1EPI',$default_immutable_headers);
_map_series_desc(qr/^AAScou.*/i,'SCOUT',$default_immutable_headers);
_map_series_desc(qr/^T1_MEMPRAGE.*/i,'MEMPRAGE',$default_immutable_headers,'EchoTime');
_map_series_desc(qr/^DIFFUSION ?$/i,'DTI',$default_immutable_headers,'SequenceName');
_map_series_desc(qr/^DIFF.*/i,'DTI',$default_immutable_headers);
_map_series_desc(qr/^ge_func.*$|^fMRI_.*$|^farself.*$|^selfref.*$|^discounting.*/i,'BOLD',$default_immutable_headers);
_map_series_desc(qr/^PhoenixZIPReport/i,'Other',$default_immutable_headers);
_map_series_desc(qr/^Stability Plots$|^RF Noise Map/i,'QA Plot',$default_immutable_headers);
_map_series_desc(qr/^epi_stability.*$|^RF_noise_eval/i,'QA Scan',$default_immutable_headers);
_map_series_desc(qr/^field_mapping.*/i,'FLDMP',$default_immutable_headers);
_map_series_desc(qr/.*FLAIR.*/i,'FLAIR',$default_immutable_headers);
_map_series_desc(qr/^SWI_.*$|^mIP_Images.*/i,'SWI',$default_immutable_headers);
_map_series_desc(qr/^DSI_.*/i,'DSI',$default_immutable_headers);

=head1 XNAT::DICOM::Series

XNAT::DICOM::Series - an collection of MRI Scans ( XNAT::DICOM::Series object, really ) that constitute a scan session.

=head2 STATIC METHODS

=over 4

=item XNAT::DICOM::Series::getAll();

Get all series ever instatiated with this class (during the current script's execution, of course).

=cut

sub getAll { shift if ref $_[0]; values %loaded_by_uid; }

=item XNAT::DICOM::Series::getByUid($uid);

Get a session by its DICOM UID as defined in the 'InstanceNumber' DICOM Header.
Returns undef if it has not been loaded or does not exist.

=cut

sub getByUid { shift if ref $_[0]; $loaded_by_uid{shift(@_)}; }

=item XNAT::DICOM::Series::getById($id);

Get a session by its Internal ID as defined by

 "${XNAT_label}"

which is the same as

 "${DICOM_PatientsName}"

Without any whitespace. Returns undef if it has not been loaded or does not exist.

=cut

sub getById { shift if ref $_[0]; $loaded_by_label{shift(@_)}; }

sub _addLoaded {
	my $self = shift || confess "Wrong number of args. ";
	my $uid = $self->{uid};
	my $label = $self->{label};
	my $errors = "";
	$errors .=  "ERROR: Series UID '$uid' already Exists.\n" if $loaded_by_uid{$uid};
	$errors .=  "ERROR: Series Internal ID '$label' already Exists.\n" if $loaded_by_label{$label};
	confess $errors if $errors;
	$loaded_by_uid{$uid} = $self;
	$loaded_by_label{$label} = $self;
}

=back

=head1 XNAT::DICOM::Series

XNAT::DICOM::Series - an collection of MRI scans (in DICOM format) that constitute a scan series.

=head2 EXAMPLE USAGE

 my $xser = XNAT::DICOM::Series->new($dicom);

Usually not instantiated directly, but automatically instantiated when the XNAT::DICOM::File object is instantiated.

=cut

=head2 INSTANCE METHODS

=over 4

=cut

=item XNAT::DICOM::Series->new($dicom)

Returns a new Series object. Can be called like so:

 my $xser = XNAT::DICOM::Series->new($dicom);

Where $dicom is a XNAT::DICOM::File object.

=cut


sub new {
	my $class = shift;
	my $self = {headers=>{},dicom_files=>{},warnings=>{},errors=>{},file_count=>0,size_on_disk=>0,uncompressed_size=>0,};
	confess "Wrong number of args." unless @_ == 1;
	bless $self,$class;
	my $dicom = shift;
	if('XNAT::DICOM::File' eq ref $dicom) {
		$self->{uid} = $dicom->getHeader('SeriesInstanceUID') || confess "Missing SeriesInstanceUID DICOM Header!";
	} else {
		confess "Did not understand arguments!\n";
	}
	$self->{session} = XNAT::DICOM::Session->new($dicom);
	$self->{series_number} = sprintf('%03d',$dicom->getHeader('SeriesNumber'));
	$self->{label} = $self->{session}->{label} . "_series_" . $self->{series_number};
	$self->{session}->addSeries($self);
	if($main::verbose > 3) {
		$self->debug(3,"XNAT::DICOM::Series->new(): label   = '$self->{label}'\n");
		$self->debug(3,"XNAT::DICOM::Series->new(): series_number = '$self->{series_number}'\n");
		$self->debug(3,"XNAT::DICOM::Series->new(): uid           = '$self->{uid}'\n");
	}
	if( getById($self->{label}) ) {
		$self->debug(3,"XNAT::DICOM::Series->new(): Already Loaded, returning previous object.\n");
		$self = getById( $self->{label} );
	} else {
		$self->debug(2,"XNAT::DICOM::Series->new(): New Series label='$self->{label}'.\n");
		$self->_addLoaded($self);
	}
	# Remember to return the $self at the end!
	$self;
}


=item $series->addDicomFile($dicom)

Add a XNAT::DICOM::File object to this series.

=cut

sub addDicomFile {
	my $self = shift;
	my $dicom = shift;
	$self->debug(1,"Adding '$dicom->{label}' to Series '$self->{label}'.\n");
	if($self->{dicom_files}->{$dicom->{file_number}}) {
		my $this_file = $dicom->{file};
		my $that_file = $self->{dicom_files}->{$dicom->{file_number}}->{file};
		if($this_file eq $that_file) {
			$self->addWarnings("Uselessly attempting to add '$this_file' again. Skipping.\n");
		} else {
			$self->addWarnings("Attempt to add duplicate dicom file '$this_file' (previously added as '$that_file'), will ignore request.\n");
		}
	} else {
		$self->{dicom_files}->{$dicom->{file_number}} = $dicom;
		$self->{_headers_checked} = 0;
		$self->{file_count}++;
		if(! $self->{directory} || $self->{directory} eq $dicom->getDirectory()) {
			$self->{directory} = $dicom->getDirectory();
		} else {
			$self->addWarnings("Adding a DICOM file in series $self->{label} from a different directory, this may cause problems.\n");
		}
		$self->addChild($dicom);
	}
}

=item $series->getSortedDicomFiles()

Get the XNAT::DICOM::Files objects that make up this Series, in numerical order.
This is slower than $series->getDicomFiles(), but much more logical. Use when outputting
for humans.

=cut

sub getSortedDicomFiles {
	my $self = shift;
	my @dicoms;
	foreach my $dicom_file_number (sort keys %{$self->{dicom_files}}) {
		push @dicoms,$self->{dicom_files}->{$dicom_file_number};
	}
	@dicoms;
}

=item $series->getDicomFiles()

Get the XNAT::DICOM::Files objects that make up this Series, in no particular order.
This is faster than $series->getSortedDicomFiles().

=cut

sub getDicomFiles { values %{$_[0]->{dicom_files}}; }

sub getXNATType {
	my $self = shift;
	# Do this only once, so if it's set, skip
	if(! exists $self->{headers}->{XNATSeriesType} ) {
		# We only need to get one dicom in the set, so let's get the first.
		my $dicom = ( $self->getDicomFiles )[0];
		# This should never happen.
		if(! $dicom) {
			confess "No DICOMS in Series, how is that possible?\n";
		}
		# Now loop through the SeriesDescription regular expressions
		foreach my $re (@_seriesDescription_2_XNAT_Type) {
			# If we have a match, then set the type
			if ( $dicom->getHeader('SeriesDescription') =~ $re ) {
				# Set the XNAT type here.
				$self->{headers}->{XNATSeriesType} = $_seriesDescription_2_XNAT_Type{$re};
				# Get the list of headers which should not change from one DICOM in this
				# series to another.
				# This should always be true, but let's check anyway.
				if($immutable_headers{$re}) {
					$self->debug(2,"Setting Immutable Headers for type '$self->{headers}->{XNATSeriesType}' ($self->{label})\n");
					$self->{immutable_headers} = $immutable_headers{$re};
				} else {
					# Something aint right, so complain.
					$self->addErrors("No Immutable Headers for type '$self->{headers}->{XNATSeriesType}' ($self->{label})\n");
					$self->{immutable_headers} = $default_immutable_headers;
				}
				# First match should be the last. Exit foreach here.
				last;
			}
		}
		# If we got here, then we don't know this SeriesDescription, most likely
		if(! $self->{immutable_headers}) {
			# Log the issue.
		    my $xnatSeriesType=$self->{headers}->{XNATSeriesType}||'[Unknown XNATSeriesType]';
		    my $label=$self->{label} || '[Missing Label]';
			$self->addWarnings("Unknown SeriesDescription='".$dicom->getHeader('SeriesDescription')."', cannot map to XNATSeriesType.\n");
			$self->debug(2,"SOMETHING'S WRONG HERE: No Immutable Headers for type '$xnatSeriesType' ($label)\n");
			# Use default headers for checking.
			$self->{immutable_headers} = $default_immutable_headers;
			$self->{headers}->{XNATSeriesType} = "";
		}
	}
	# Return this, though I don't know if we'll ever use it this way.
	$self->debug(4,"XNATSeriesType = '$self->{headers}->{XNATSeriesType}' for ($self->{label})\n");
	$self->{headers}->{XNATSeriesType};
}

=item $series->getDirectory()

Returns the directory in which the DICOM file is located.

=cut

sub getDirectory { $_[0]->{directory}; }

=item $series->getPrearchiveDirectory()

Returns the best guess as to what is the PreArchive directory of the DICOM file.

=cut

sub getPreArchiveDirectory { $_[0]->{prearchive_dir}; }

=item $series->checkHeaders()

Check and see that the headers are OK.

=cut

sub checkHeaders {
	my $self = shift;
	if(! $self->{_headers_checked}) {
		my @dicoms = $self->getDicomFiles();
		#$self->{headers} = {};
		$self->getXNATType if ! $self->{immutable_headers};
		$self->debug(4,"XNATSeriesType (1) = '$self->{headers}->{XNATSeriesType}' for ($self->{label})\n");
		my @headers = keys %{$self->{immutable_headers}};
		foreach my $header ( @headers ) {
			$self->debug(3,"Checking $header for Series: $self->{label}\n");
			foreach my $dicom ( @dicoms ) {
				my $this_header_value = $dicom->getHeader($header);
				if(! defined $this_header_value ) {
					$this_header_value = '';
				}
				if(exists $self->{headers}->{$header}) {
					if($self->{headers}->{$header} ne $this_header_value) {
						$self->addWarnings("Header $header with value of '$this_header_value' does not match previously read ".
															 "value of '$self->{headers}->{$header}'.\n");
					}
				} else {
					$self->{headers}->{$header} = $this_header_value;
				}
			}
		}
	$self->debug(4,"XNATSeriesType (2) = '$self->{headers}->{XNATSeriesType}' for ($self->{label})\n");
	}
	$self->debug(4,"XNATSeriesType (3) = '$self->{headers}->{XNATSeriesType}' for ($self->{label})\n");
}

=item $series->getSliceOrientation()

Get the calculated orientation of the image slice.
Returns 'Tra','Sag','Cor' or '' if we could not compute it or it was not computed for some reason.

=cut

sub getSliceOrientation { $_[0]->{'XNATSliceOrientation'}; }

=item $series->setSliceOrientation()

Set the calculated orientation of the image slice. Should be one of 'Tra','Sag','Cor' or ''

=cut

sub setSliceOrientation { $_[0]->{'XNATSliceOrientation'} = $_[1]; }

=item $series->getHeader($header)

Get the common DICOM header value for this series. Returns value, 'Several', if there are more than one, or undef.

=cut

sub getHeader { my($self,$header) = @_;$self->{headers}->{$header}; }


=item $series->getHeader($header)

Get the common DICOM header value for this series. Will set it to 'Several', if there are more than one.

=cut

sub setHeader {
	my($self,$header,$value,$xtra) = @_;
	if($xtra) {
		confess "ERROR: Wrong numbre of arguments!\n";
	}
	if( exists $self->{headers}->{$header} && $self->{headers}->{$header} ne $value) {
		$self->{headers}->{$header} = 'Several';
	} else {
		$self->{headers}->{$header} = $value;
	}
}

sub check {
	my $self = shift;
	$self->checkHeaders;
	print "Series: $self->{label} ('$self->{headers}->{SeriesDescription}' ; '$self->{headers}->{XNATSeriesType}' ) $self->{file_count} files\n";
	print $self->getErrors if $self->hadErrors;
	print $self->getWarnings if $self->hadWarnings;
}

sub getSessionXML {
	my $self = shift;
	my $opts = shift;
	my $xml = "";
	printf " - $self->{label} %-24s %-15s - %5s files\n","(".$self->getHeader('SeriesDescription').")",($self->getHeader('XNATSeriesType') ? $self->getHeader('XNATSeriesType') : 'UNKNOWN'), $self->{file_count} if $main::verbose > 0;
	# $xnat_data{ReceiveCoilType} = $xnat_data{ReceiveCoilType} || 'null';
	foreach my $header (qw(XResolution YResolution SliceThickness RepetitionTime EchoTime InversionTime )) {
		if ($self->getHeader($header)) {
			$self->{headers}->{$header} = sprintf('%0.7f',$self->{headers}->{$header});
			$self->{headers}->{$header} =~ s/(\.\d.*?)0+$/$1/;
		}
	}
	if (($self->getHeader('PatientPosition') && $self->getHeader('PatientPosition') ne 'HFS') && $self->getHeader('SeriesDescription') ne 'PhoenixZIPReport') {
		$self->addWarnings("DICOM Header PatientPosition='".$self->getHeader('PatientPosition')."', I only understand 'HFS'!");
	}
	$self->{headers}->{SeriesTime} =~ s/\.\d+$//;

	# Output the series section in the session's main XML
	if( $self->getHeader('XNATSeriesType')  && $opts->{extended_output} ) {
		$xml .= "   <xnat:scan ID=\"".$self->getHeader('SeriesNumber')."\" type=\"".$self->getHeader('XNATSeriesType')."\" xsi:type=\"xnat:mrScanData\">\n";
	} else {
		$xml .= "   <xnat:scan ID=\"".$self->getHeader('SeriesNumber')."\" xsi:type=\"xnat:mrScanData\">\n";
	}
	$xml .= "     <xnat:series_description>".$self->getHeader('SeriesDescription')."</xnat:series_description>\n";
	$xml .= "     <xnat:scanner manufacturer=\"".$self->getHeader('Manufacturer')."\" model=\"".$self->getHeader('ManufacturersModelName')."\">".$self->getHeader('StationName')."</xnat:scanner>\n";
	$xml .= "     <xnat:file URI=\"scan_".$self->getHeader('SeriesNumber')."_catalog.xml\" content=\"RAW\" format=\"DICOM\" xsi:type=\"xnat:resourceCatalog\" />\n";
	$xml .= "     <xnat:coil>".$self->getHeader('ReceiveCoilType')."</xnat:coil>\n";
	$xml .= "     <xnat:fieldStrength>".$self->getHeader('MagneticFieldStrength')."</xnat:fieldStrength>\n";
	$xml .= "     <xnat:parameters>\n";
	if($self->getHeader('XResolution')) {
		$xml .= "       <xnat:voxelRes x=\"".$self->getHeader('XResolution')."\" y=\"".$self->getHeader('XResolution')."\" z=\"".$self->getHeader('SliceThickness')."\" />\n";
	}
	if( $self->getHeader('XNATSliceOrientation') && $self->getHeader('XNATSliceOrientation') ne 'Several' ) {
		$xml .= "       <xnat:orientation>".$self->getHeader('XNATSliceOrientation')."</xnat:orientation>\n";
	}
	if($self->getHeader('Columns')) {
		$xml .= "       <xnat:fov x=\"".$self->getHeader('Columns')."\" y=\"".$self->getHeader('Rows')."\" />\n";
	}
	if($self->getHeader('RepetitionTime')) {
		$xml .= "       <xnat:tr>".$self->getHeader('RepetitionTime')."</xnat:tr>\n";
	}
	$xml .= "       <xnat:te>".$self->getHeader('EchoTime')."</xnat:te>\n" if $self->getHeader('EchoTime') && $self->getHeader('EchoTime') > 0;
	if($self->getHeader('InversionTime') ) {
		$xml .= "       <xnat:ti>".$self->getHeader('InversionTime')."</xnat:ti>\n" if $self->getHeader('InversionTime');
		$xml .= "       <xnat:flip>".$self->getHeader('FlipAngle')."</xnat:flip>\n" if $self->getHeader('FlipAngle');
		$xml .= "       <xnat:sequence>".$self->getHeader('SequenceName')."</xnat:sequence>\n" if $self->getHeader('SequenceName');
	}
	$xml .= "       <xnat:scanTime>".$self->getHeader('SeriesTime')."</xnat:scanTime>\n";
	if($self->getHeader('InversionTime')) {
		$xml .= "       <xnat:imageType>".$self->getHeader('ImageType')."</xnat:imageType>\n" if $self->getHeader('ImageType');
		$xml .= "       <xnat:scanSequence>".$self->getHeader('ScanningSequence')."</xnat:scanSequence>\n" if $self->getHeader('ScanningSequence');
		$xml .= "       <xnat:seqVariant>".$self->getHeader('SequenceVariant')."</xnat:seqVariant>\n" if $self->getHeader('SequenceVariant');
		$xml .= "       <xnat:scanOptions>".$self->getHeader('ScanOptions')."</xnat:scanOptions>\n" if $self->getHeader('ScanOptions');
		$xml .= "       <xnat:acqType>".$self->getHeader('MRAcquisitionType')."</xnat:acqType>\n" if $self->getHeader('MRAcquisitionType');
	}
	$xml .= "     </xnat:parameters>\n";
	$xml .= "   </xnat:scan>\n";
}

sub getSeriesXML {
	my $self = shift;
	# Now create the series catalog XML
	#my $catalog_xml_filename = "$prearchive_dir/scan_".$self->getHeader('SeriesNumber')."_catalog.xml";
	$self->{xml} = "";
	$self->{xml} .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
	$self->{xml} .= "<cat:DCMCatalog UID=\"".$self->getHeader('SeriesInstanceUID')."\"";
	$self->{xml} .= " xmlns:arc=\"http://nrg.wustl.edu/arc\"";
	$self->{xml} .= " xmlns:behavioral=\"http://nrg.wustl.edu/behavioral\"";
	$self->{xml} .= " xmlns:cat=\"http://nrg.wustl.edu/catalog\"";
	$self->{xml} .= " xmlns:cnda=\"http://nrg.wustl.edu/cnda\"";
	$self->{xml} .= " xmlns:fs=\"http://nrg.wustl.edu/fs\"";
	$self->{xml} .= " xmlns:kblack=\"http://nrg.wustl.edu/wu_kblack\"";
	$self->{xml} .= " xmlns:ls2=\"http://nrg.wustl.edu/ls2\"";
	$self->{xml} .= " xmlns:prov=\"http://www.nbirn.net/prov\"";
	$self->{xml} .= " xmlns:wrk=\"http://nrg.wustl.edu/workflow\"";
	$self->{xml} .= " xmlns:xdat=\"http://nrg.wustl.edu/security\"";
	$self->{xml} .= " xmlns:xnat=\"http://nrg.wustl.edu/xnat\"";
	$self->{xml} .= " xmlns:xnat_a=\"http://nrg.wustl.edu/xnat_assessments\"";
	$self->{xml} .= " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n";
	$self->{xml} .= "  <cat:entries>\n";
	foreach my $dicom ( $self->getSortedDicomFiles ) {
		my $filename = $dicom->{catalog_filename};
		my $instanceNumber = $dicom->getHeader('InstanceNumber');
		my $uid = $dicom->getUid;
		$self->{xml} .= "    <cat:entry UID=\"$uid\" URI=\"$filename\" instanceNumber=\"$instanceNumber\" xsi:type=\"cat:dcmEntry\"/>\n";
	}
	$self->{xml} .= "  </cat:entries>\n";
	if($self->getHeader('Columns')) {
		$self->{xml} .= "  <cat:dimensions volumes=\"1\"  x=\"".$self->getHeader('Columns')."\" y=\"".$self->getHeader('Rows')."\" z=\"$self->{file_count}\" />\n";
	} else {
		$self->{xml} .= "  <cat:dimensions volumes=\"1\" z=\"$self->{file_count}\" />\n";
	}
	if($self->{XResolution}) {
		$self->{xml} .= "  <cat:voxelRes x=\"$self->{XResolution}\" y=\"$self->{XResolution}\" z=\"".$self->getHeader('SliceThickness')."\" />\n";
	}
	if( $self->{SliceOrientation} && $self->{SliceOrientation} ne 'Several' ) {
		$self->{xml} .= "  <cat:orientation>$self->{SliceOrientation}</cat:orientation>\n";
	}
	$self->{xml} .= "</cat:DCMCatalog>\n";
}

=back

=cut

1;
__END__
